#include "../../include/graphics/gl/GLRenderingCore.hpp"
#include "../../include/graphics/gl/GLTexture.hpp"
#include "../../include/graphics/gl/GLFont.hpp"
#include "../../include/error/Exception.hpp"
using namespace klas;
using namespace graphics;
using namespace gl;

const uint32 GLRenderingCore::IDX_TEXTURE   = 0;
const uint32 GLRenderingCore::IDX_CIRCLE    = 1;
const uint32 GLRenderingCore::IDX_TRIANGLE  = 2;
const uint32 GLRenderingCore::IDX_LINE      = 3;

#include <iostream>
using std::cout;
using std::endl;

GLRenderingCore::GLRenderingCore(uint32 sizeHint)
{

  // Make sure we have VBOs
  if(GL_ARB_vertex_buffer_object == false)
    throw error::Exception("GLRenderingCore exception: OpenGL driver does not have VBO (vertex buffer object) support.");
 
  // Generate our vertex buffers

  // actually generate buffers
  m_buffers = new GLuint(4);
  glGenBuffers(4, m_buffers);

  /* TEXTURE VBO */
  {
    // vertex and texture positions, using triangle strips
    GLdouble vertexData[] = {0.0, 1.0,
                             0.0, 0.0,
                             1.0, 1.0,
                             1.0, 0.0};

    GLdouble textureData[] = {0.0, 1.0,
                              0.0, 0.0,
                              1.0, 1.0,
                              1.0, 0.0};


    // Bind the buffer
    glBindBuffer(GL_ARRAY_BUFFER, m_buffers[IDX_TEXTURE]);

    // Copy data over
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData) + sizeof(textureData), 0, GL_STATIC_DRAW);
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertexData), vertexData);
    glBufferSubData(GL_ARRAY_BUFFER, sizeof(vertexData), sizeof(textureData), textureData);

    // Tell openGL about where that data is
    glTexCoordPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL + sizeof(vertexData)));
    glVertexPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL));

    // Finished with this VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
  }


  /* CIRCLE VBO */
  {
    // Compute vertex position
    GLdouble vertexData[360 * 2]; // 360 degrees, 2 coordinates per degree
    for(int i = 0; i < 720; i += 2)
    {
      double degInRad = (i / 2.0) * (PI_OVER_180);
      vertexData[i] = cos(degInRad) + .5;
      vertexData[i+1] = sin(degInRad) + .5;
    }

    // Bind the buffer
    glBindBuffer(GL_ARRAY_BUFFER, m_buffers[IDX_CIRCLE]);

    // Copy data over
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);

    // Tell OpenGL where the data is.
    glVertexPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL));

    // Finished with the VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
  }

  /* TRIANGLE VBO */
  {
    GLdouble vertexData[180 * 6];
    for(int i = 0; i < (180 * 6); i += 6)
    {
      double degInRad = (i / 6.0) * (PI_OVER_180);

      // pt 1
      vertexData[i] = 0;      // x
      vertexData[i+1] = 0;    // y

      // pt 2
      vertexData[i+2] = 1;
      vertexData[i+3] = 0;
      
      // pt 3
      vertexData[i+4] = cos(degInRad);
      vertexData[i+5] = sin(degInRad);

      // make the points centered at (.5, .5) so they rotate correctly
      double centerX = (vertexData[i] + vertexData[i+2] + vertexData[i+4]) / 3.0;
      double centerY = (vertexData[i+1] + vertexData[i+3] + vertexData[i+5]) / 3.0;

      double distX = .5 - centerX;
      double distY = .5 - centerY;

      // adjust the pointers so they are centered
      vertexData[i] += distX;
      vertexData[i+1] += distY;

      vertexData[i+2] += distX;
      vertexData[i+3] += distY;

      vertexData[i+4] += distX;
      vertexData[i+5] += distY;

    }

    // Bind the buffer
    glBindBuffer(GL_ARRAY_BUFFER, m_buffers[IDX_TRIANGLE]);

    // Copy data over
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);

    // Tell OpenGL where the data is.
    glVertexPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL));

    // Finished with the VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
  }

  /* LINE VBO */
  {
    // Generate line position
    GLdouble vertexData[4];
    vertexData[0] = 0;  // x1
    vertexData[1] = .5; // y1

    vertexData[2] = 1;  // x2
    vertexData[3] = .5; // y2

    // Bind the buffer
    glBindBuffer(GL_ARRAY_BUFFER, m_buffers[IDX_LINE]);

    // Copy data over
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);

    // Tell OpenGL where the data is.
    glVertexPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL));

    // Finished with the VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
  }



  // Clear out our storage containers.
  this->clearCache();

  // Reserve data to avoid painful container resizes.
  m_text.reserve(sizeHint);
  m_textures.reserve(sizeHint);
  m_primitives.reserve(sizeHint);  

}

GLRenderingCore::~GLRenderingCore()
{
  // Delete vbos if they are created
  if(m_buffers)
  {
    glDeleteBuffers(4, m_buffers);
    klasDeleteArr(m_buffers);
  }
}

void GLRenderingCore::add(string text, Font* font, const Color& col, const Transformation& transform, double transparency, uint16 layer)
{
  RenderingCore::internalText txt;
  txt.text = text;
  txt.font = font;
  txt.col = col;
  txt.transform = transform;
  txt.layer = layer;
  txt.transparency = transparency;

  m_text.push_back(txt);
}

void GLRenderingCore::add(Texture* tex, const Transformation& transform, double transparency, const Color& col, uint16 layer)
{
  RenderingCore::internalTexture texture;
  texture.tex = tex;
  texture.transform = transform;
  texture.layer = layer;
  texture.col = col;
  texture.transparency = transparency;

  m_textures.push_back(texture);
}

void GLRenderingCore::add(primitiveTypes::sPrimitiveType type, const Transformation& transform, const Color& col, double transparency, uint16 layer)
{
  RenderingCore::internalPrimitive prim;
  prim.type = type;
  prim.col = col;
  prim.transform = transform;
  prim.layer = layer;
  prim.transparency = transparency;

  m_primitives.push_back(prim);
}

// Clear out our vectors
void GLRenderingCore::clearCache(bool removeText)
{
  if(removeText)
    m_text.clear();

  m_textures.clear();
  m_primitives.clear();
}

void GLRenderingCore::applyTransform(const Transformation& t, const math::Rectd& cam)
{
  /*  scaling transformations (ie: setScale)
  glTranslated((t.pos.x - cam.x), (t.pos.y - cam.y), 0.0f);
  glRotated(t.rot, 0.0f, 0.0f, 1.0f);
  glTranslated(-(t.pos.x - cam.x), -(t.pos.y - cam.y), 0.0f);
  glTranslated((t.pos.x - ((rectSize.x * t.size.x) / 2.0) - cam.x), ((t.pos.y - (rectSize.y * t.size.y) / 2.0) - cam.y), 0.0f);
  glScaled(t.size.x, t.size.y, 0.0);*/


  /*  who knows
  glTranslated((t.pos.x - cam.x), (t.pos.y - cam.y), 0.0f);
  glRotated(t.rot, 0.0f, 0.0f, 1.0f);
  glTranslated(-(t.pos.x - cam.x) / 2.0, -(t.pos.y - cam.y) / 2.0, 0.0f);
  glTranslated((t.pos.x - ((rectSize.x * t.size.x) / 2.0) - cam.x), ((t.pos.y - (rectSize.y * t.size.y) / 2.0) - cam.y), 0.0f);
  glScaled(t.size.x, t.size.y, 0.0);*/

  /* literal size transformations (ie: setSize) */
  glTranslated((t.pos.x - cam.x), (t.pos.y - cam.y), 0.0);
  glRotated(t.rot, 0.0, 0.0, 1.0);
  glTranslated(-(t.pos.x - cam.x) +     (t.pos.x - ((t.size.x) / 2.0) - cam.x),
               -(t.pos.y - cam.y) +     (t.pos.y - ((t.size.y) / 2.0) - cam.y), 0.0);
  glScaled(t.size.x, t.size.y, 0.0);
}


void GLRenderingCore::render(const math::Rectd& cam, klas::uint16 layer)
{
  /* TEXTURES -------------------------------------------------------------------------------------------------- */
  glEnable(GL_TEXTURE_2D);

  glEnableClientState(GL_TEXTURE_COORD_ARRAY);
  glEnableClientState(GL_VERTEX_ARRAY);

  glBindBuffer(GL_ARRAY_BUFFER, m_buffers[IDX_TEXTURE]);
  glTexCoordPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL + (8 * sizeof(GLfloat))));
  glVertexPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL));

  for(vector<RenderingCore::internalTexture>::iterator it = m_textures.begin(); it != m_textures.end(); ++it)
  {
    // Push matrix
    glPushMatrix();

    // set up the proper color
    glColor4d(it->col.r, it->col.g, it->col.b, it->col.a * it->transparency);

    // transform the current matrix
    applyTransform(it->transform, cam);

    // activate the proper texture and render it
    dynamic_cast<GLTexture*>(it->tex)->activate();
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    // Pop matrix
    glPopMatrix();
  }
  glDisableClientState(GL_TEXTURE_COORD_ARRAY);
  glDisableClientState(GL_VERTEX_ARRAY);
  glDisable(GL_TEXTURE_2D);
  glBindBuffer(GL_ARRAY_BUFFER, 0);

  /* TEXTURES END ----------------------------------------------------------------------------------------------- */
  
  /* PRIMITIVES BEGIN ------------------------------------------------------------------------------------------- */
  glEnableClientState(GL_VERTEX_ARRAY);

  primitiveTypes::ePolygonMode polygonMode = primitiveTypes::FILLED;
  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

  for(vector<RenderingCore::internalPrimitive>::iterator it = m_primitives.begin(); it != m_primitives.end(); ++it)
  {
    glPushMatrix();
    
    // set the color of the primitive
    glColor4d(it->col.r, it->col.g, it->col.b, it->col.a * it->transparency);

    // transform the current matrix
    applyTransform(it->transform, cam);

    // change the polygon rendering mode
    switch(it->type.polygonMode)
    {
      case primitiveTypes::FILLED:
      {
        if(polygonMode != primitiveTypes::FILLED)
        {
          glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
          polygonMode = primitiveTypes::FILLED;
        }
        break;
      }
      case primitiveTypes::WIREFRAME:
      {
        if(polygonMode != primitiveTypes::WIREFRAME)
        {
          glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
          polygonMode = primitiveTypes::WIREFRAME;
        }
        break;
      }
      case primitiveTypes::VERTICIES:
      {
        if(polygonMode != primitiveTypes::VERTICIES)
        {
          glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);
          polygonMode = primitiveTypes::VERTICIES;
        }
        break;
      } 
    }

    // render the shape
    switch(it->type.primitiveType)
    {
      case primitiveTypes::CIRCLE:
      {
        glBindBuffer(GL_ARRAY_BUFFER, m_buffers[IDX_CIRCLE]);
        glVertexPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL));
        glDrawArrays(GL_POLYGON, 0, 360);
        break;
      }
      case primitiveTypes::TRIANGLE:
      {
        glBindBuffer(GL_ARRAY_BUFFER, m_buffers[IDX_TRIANGLE]);
        glVertexPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL));
        glDrawArrays(GL_TRIANGLES, GLint(it->type.angle * 3), 3);
        break;
      }
      case primitiveTypes::RECTANGLE:
      {
        glBindBuffer(GL_ARRAY_BUFFER, m_buffers[IDX_TEXTURE]);
        glVertexPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL));
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        break;
      }
      case primitiveTypes::LINE:
      {
        glBindBuffer(GL_ARRAY_BUFFER, m_buffers[IDX_LINE]);
        glVertexPointer(2, GL_DOUBLE, 0, (GLvoid*)((char*)NULL));
        glDrawArrays(GL_LINES, 0, 2);
        break;
      }

    }

    glPopMatrix();
  }

  glDisableClientState(GL_VERTEX_ARRAY);
  glBindBuffer(GL_ARRAY_BUFFER, 0);

  /* PRIMITIVES END --------------------------------------------------------------------------------------------- */


  /* FONTS START ------------------------------------------------------------------------------------------------ */
  for(vector<RenderingCore::internalText>::iterator it = m_text.begin(); it != m_text.end(); ++it)
  {
    glPushMatrix();

    // set the color of the primitive
    glColor4d(it->col.r, it->col.g, it->col.b, it->col.a * it->transparency);

    // transform the current matrix
    applyTransform(it->transform, cam);

    // render the text
    dynamic_cast<GLFont*>(it->font)->render(it->text);

    glPopMatrix();
  }
}